package org.kazuhiko.pokemon3d.utility.camera;

import static org.lwjgl.opengl.ARBDepthClamp.GL_DEPTH_CLAMP;
import static org.lwjgl.opengl.GL11.GL_MODELVIEW;
import static org.lwjgl.opengl.GL11.GL_PROJECTION;
import static org.lwjgl.opengl.GL11.GL_TRANSFORM_BIT;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glLoadIdentity;
import static org.lwjgl.opengl.GL11.glMatrixMode;
import static org.lwjgl.opengl.GL11.glOrtho;
import static org.lwjgl.opengl.GL11.glPopAttrib;
import static org.lwjgl.opengl.GL11.glPushAttrib;
import static org.lwjgl.opengl.GL11.glRotatef;
import static org.lwjgl.opengl.GL11.glTranslatef;

import java.text.DecimalFormat;

import org.kazuhiko.pokemon3d.entity.Collidable;
import org.kazuhiko.pokemon3d.entity.Drawable;
import org.kazuhiko.pokemon3d.game.World;
import org.kazuhiko.pokemon3d.physics.Collision;
import org.kazuhiko.pokemon3d.physics.Collision.Side;
import org.kazuhiko.pokemon3d.utility.tools.Debugger;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.GLContext;
import org.lwjgl.util.glu.GLU;
import org.lwjgl.util.vector.Vector3f;
import org.newdawn.slick.Color;

public class PlayerCamera
{

	private static final float	WALK_SPEED			= 0.05f;
	private static final float	RUN_SPEED			= 0.2f;
	private static final float	SPRINT_SPEED		= 0.4f;

	private static float		SPEED				= RUN_SPEED;

	private final float			gravity				= 0.0175f;

	private DecimalFormat		formatter			= new DecimalFormat( "#.##" );

	private Vector3f			position			= new Vector3f();
	private Vector3f			rotation			= new Vector3f();
	private Vector3f			previousPosition	= new Vector3f();
	private Vector3f			followDistance		= new Vector3f( 0, - 5f, 0 );

	private boolean				moveForward			= false;
	private boolean				moveBackward		= false;
	private boolean				strafeLeft			= false;
	private boolean				strafeRight			= false;
	private boolean				onGround			= false;
	private boolean				jumping				= false;
	private boolean				falling				= false;
	private boolean				walking				= false;
	private boolean				running				= true;
	private boolean				sprinting			= false;

	private float				yBottom				= 0;
	private float				yMotion				= 0;
	private float				fov					= 70;
	private float				aspectRatio;
	private float				zFar				= 100;
	private float				zNear				= 0.3f;

	private Drawable			followObject;

	private Debugger			debugger			= Debugger.getInstance();

	private World				world;

	public PlayerCamera()
	{

	}

	public PlayerCamera( float aspectRatio, float x, float y, float z )
	{
		this.aspectRatio = aspectRatio;
		this.position.x = x;
		this.position.y = y;
		this.position.z = z;
	}

	public void followObject( Drawable followObject )
	{
		this.position = followObject.getPosition();
		this.followDistance = new Vector3f( 0, - followObject.getHeight() + 1f, 0 );
		this.rotation = new Vector3f( 0, followObject.getRotation().getY(), 0 );
		this.followObject = followObject;
	}

	public Drawable getFollowObject()
	{
		return followObject;
	}

	public void setFieldOfView( float fov )
	{
		this.fov = fov;
	}

	public void translateCamera()
	{
		glPushAttrib( GL_TRANSFORM_BIT );
		glMatrixMode( GL_MODELVIEW );
		glRotatef( rotation.x, 1, 0, 0 );
		glRotatef( rotation.y, 0, 1, 0 );
		glRotatef( rotation.z, 0, 0, 1 );
		glTranslatef( - position.x + followDistance.x, - position.y + followDistance.y, - position.z + followDistance.z );
		glPopAttrib();
	}

	

	public Vector3f getFollowDistance()
	{
		return followDistance;
	}

	public void setFollowDistance( Vector3f followDistance )
	{
		this.followDistance = followDistance;
	}

	public float getX()
	{
		return position.x;
	}

	public float getY()
	{
		return position.y;
	}

	public float getZ()
	{
		return position.z;
	}

	public float getPitch()
	{
		return rotation.x;
	}

	public float getYaw()
	{
		return rotation.y;
	}

	public float getRoll()
	{
		return rotation.z;
	}

	public void update( int delta )
	{
		// TODO: implement with delta
		updatePreviousVector();
		updateMotion();
		input();
		if( followObject != null )
		{
			collisions();
			individualPhysics();
			updateFollowObject();
		}

		debugger.addDebugText( "Camera", Color.red );
		debugger.addDebugText( "X: " + formatter.format( getX() ), Color.yellow );
		debugger.addDebugText( "Y: " + formatter.format( getY() ), Color.yellow );
		debugger.addDebugText( "Z: " + formatter.format( getZ() ), Color.yellow );
		debugger.addDebugText( "Pitch: " + formatter.format( getPitch() ), Color.yellow );
		debugger.addDebugText( "Yaw: " + formatter.format( getYaw() ), Color.yellow );
		debugger.addDebugText( "Roll: " + formatter.format( getRoll() ), Color.yellow );
		debugger.addDebugText( "speed: " + SPEED, Color.yellow );

		if ( followObject != null )
		{
			debugger.addDebugText( "Following", Color.red );
			debugger.addDebugText( "ID: #" + followObject.getId(), Color.yellow );
			debugger.addDebugText( "Name: " + followObject.getName(), Color.yellow );
			debugger.addDebugText( "X: " + formatter.format( followObject.getPosition().getX() ), Color.yellow );
			debugger.addDebugText( "Y: " + formatter.format( followObject.getPosition().getY() ), Color.yellow );
			debugger.addDebugText( "Z: " + formatter.format( followObject.getPosition().getZ() ), Color.yellow );
			debugger.addDebugText( "Pitch: " + formatter.format( followObject.getRotation().x ), Color.yellow );
			debugger.addDebugText( "Yaw: " + formatter.format( followObject.getRotation().y ), Color.yellow );
			debugger.addDebugText( "Roll: " + formatter.format( followObject.getRotation().z ), Color.yellow );
			debugger.addDebugText( "Distance X: " + formatter.format( followDistance.getX() ), Color.yellow );
			debugger.addDebugText( "Distance Y: " + formatter.format( followDistance.getY() ), Color.yellow );
			debugger.addDebugText( "Distance Z: " + formatter.format( followDistance.getZ() ), Color.yellow );
		}
	}

	public void individualPhysics()
	{
		onGround = ( position.y == yBottom && ! jumping && ! falling );
		jumping = ( yMotion > 0 );
		falling = ( yMotion < 0 );
		position.y += yMotion;
		if ( position.y > yBottom )
		{
			yMotion -= gravity;
		}
		if ( position.y <= yBottom )
		{
			position.y = yBottom;
			yMotion = 0;
		}
	}

	private void updateFollowObject()
	{
		if ( followObject != null )
		{
			followObject.setPosition( new Vector3f( position.x, position.y, position.z ) );

			followObject.setYaw( rotation.y );
		}
	}

	public void applyOrthographicMatrix()
	{
		glPushAttrib( GL_TRANSFORM_BIT );
		glMatrixMode( GL_PROJECTION );
		glLoadIdentity();
		glOrtho( - aspectRatio, aspectRatio, - 1, 1, 0, zFar );
		glPopAttrib();
	}

	public void applyOptimalStates()
	{
		if ( GLContext.getCapabilities().GL_ARB_depth_clamp )
		{
			glEnable( GL_DEPTH_CLAMP );
		}
	}

	public void applyPerspectiveMatrix()
	{
		glPushAttrib( GL_TRANSFORM_BIT );
		glMatrixMode( GL_PROJECTION );
		glLoadIdentity();
		GLU.gluPerspective( fov, aspectRatio, zNear, zFar );
		glPopAttrib();
	}

	public void collisions()
	{
		if( !onTopOfCollidable() )
		{
			yBottom = 0;
		}
		
		for( Drawable obj : world.getDrawables() )
		{
			if( !obj.equals( followObject ) && obj instanceof Collidable )
			{
				for( Collision col : obj.getCollisions() )
				{					
					if( followObject.getPosition().getX() - followObject.getWidth() < obj.getPosition().getX() + col.getX() + col.getWidth() && followObject.getPosition().getX() + followObject.getWidth() > obj.getPosition().getX() + col.getX() )
					{	
						debugger.addDebugText( "Collision", Color.red );
						debugger.addDebugText( "Side: " + col.getSide(), Color.yellow );
						
						if( col.getSide() == Side.FRONT )
						{	
							if( -( obj.getPosition().getZ() + ( obj.getDepth() / 2 ) - col.getZ() ) > followObject.getPosition().z && -( obj.getPosition().getZ() + ( obj.getDepth() / 2 ) - col.getZ() + col.getDepth() ) < followObject.getPosition().z )
							{
								
								
								position.x = previousPosition.x;
								position.z = previousPosition.z -0.1f;
							}
						}
					}
				}
			}
		}
		/*
		for( Drawable obj : world.getDrawables() )
		{
			if( !obj.equals( followObject ) && obj instanceof Collidable )
			{
				if( Math.abs( position.x - obj.getPosition().getX() ) < ( obj.getWidth() / 2 ) && Math.abs( position.z - obj.getPosition().getZ() ) < ( obj.getDepth() / 2 ) )
				{
					if( Math.abs( position.y - obj.getYTop() * 2 ) < ( obj.getHeight() / 2 ) || position.y > obj.getYTop() - 0.5f )
					{
						yBottom = obj.getYTop();
					}
					else
					{
						position.x = previousPosition.x;
						position.z = previousPosition.z;
					}
				}
			}
		}
		*/
	}
	
	private boolean onTopOfCollidable()
	{
		/*
		for( Drawable obj : world.getDrawables() )
		{
			if( !obj.equals( followObject ) && obj instanceof Collidable )
			{
				if( Math.abs( position.x - obj.getPosition().getX() ) < ( obj.getWidth() / 2 ) && Math.abs( position.z - obj.getPosition().getZ() ) < ( obj.getDepth() / 2 ) )
				{
					return true;
				}
			}
		}
		*/
		return false;
	}

	public void input()
	{
		if ( Keyboard.isKeyDown( Keyboard.KEY_W ) )
		{
			moveForward = true;
		}
		else
		{
			moveForward = false;
		}

		if ( Keyboard.isKeyDown( Keyboard.KEY_S ) )
		{
			moveBackward = true;
		}
		else
		{
			moveBackward = false;
		}

		if ( Keyboard.isKeyDown( Keyboard.KEY_A ) )
		{
			strafeLeft = true;
		}
		else
		{
			strafeLeft = false;
		}

		if ( Keyboard.isKeyDown( Keyboard.KEY_D ) )
		{
			strafeRight = true;
		}
		else
		{
			strafeRight = false;
		}

		if ( Keyboard.isKeyDown( Keyboard.KEY_LSHIFT ) && ! Keyboard.isKeyDown( Keyboard.KEY_LCONTROL ) )
		{
			sprinting = true;
			running = false;
			walking = false;
		}
		else if ( ! Keyboard.isKeyDown( Keyboard.KEY_LSHIFT ) && Keyboard.isKeyDown( Keyboard.KEY_LCONTROL ) )
		{
			walking = true;
			sprinting = false;
			running = false;
		}
		else if ( ! Keyboard.isKeyDown( Keyboard.KEY_LSHIFT ) && ! Keyboard.isKeyDown( Keyboard.KEY_LCONTROL ) )
		{
			walking = false;
			sprinting = false;
			running = true;
		}

		if ( Keyboard.isKeyDown( Keyboard.KEY_SPACE ) && onGround )
		{
			yMotion = 0.3f;
		}

		if ( Mouse.isGrabbed() )
		{
			float mouseDX = Mouse.getDX() * 0.8f * 0.16f;
			float mouseDY = Mouse.getDY() * 0.8f * 0.16f;
			if ( rotation.y + mouseDX >= 360 )
			{
				rotation.y = rotation.y + mouseDX - 360;
			}
			else if ( rotation.y + mouseDX < 0 )
			{
				rotation.y = 360 - rotation.y + mouseDX;
			}
			else
			{
				rotation.y += mouseDX;
			}
			if ( rotation.x - mouseDY >= - 89 && rotation.x - mouseDY <= 89 )
			{
				rotation.x += - mouseDY;
			}
			else if ( rotation.x - mouseDY < - 89 )
			{
				rotation.x = - 89;
			}
			else if ( rotation.x - mouseDY > 89 )
			{
				rotation.x = 89;
			}
		}
	}

	public void updatePreviousVector()
	{
		previousPosition.x = position.x;
		previousPosition.y = position.y;
		previousPosition.z = position.z;
	}

	public void updateMotion()
	{
		if ( walking && ! sprinting )
		{
			SPEED = WALK_SPEED;
		}
		else if ( ! walking && sprinting )
		{
			SPEED = SPRINT_SPEED;
		}
		else if ( running )
		{
			SPEED = RUN_SPEED;
		}
		if ( moveForward )
		{
			position.x += SPEED * Math.sin( rotation.y * Math.PI / 180 );
			position.z += SPEED * - Math.cos( rotation.y * Math.PI / 180 );
		}
		if ( moveBackward )
		{
			position.x -= SPEED * Math.sin( rotation.y * Math.PI / 180 );
			position.z -= SPEED * - Math.cos( rotation.y * Math.PI / 180 );
		}
		if ( strafeLeft )
		{
			position.x += SPEED * Math.sin( ( rotation.y - 90 ) * Math.PI / 180 );
			position.z += SPEED * - Math.cos( ( rotation.y - 90 ) * Math.PI / 180 );
		}
		if ( strafeRight )
		{
			position.x += SPEED * Math.sin( ( rotation.y + 90 ) * Math.PI / 180 );
			position.z += SPEED * - Math.cos( ( rotation.y + 90 ) * Math.PI / 180 );
		}
	}

	public void setWorld( World world )
	{
		this.world = world;
	}
}